#!/usr/bin/perl

###############################################################################
# This file is part of Ásbrú Connection Manager
#
# Copyright (C) 2017-2022 Ásbrú Connection Manager team (https://asbru-cm.net)
# Copyright (C) 2010-2016 David Torrejón Vaquerizas
#
# Ásbrú Connection Manager is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ásbrú Connection Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with Ásbrú Connection Manager.
# If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.
###############################################################################

use utf8;

$|++;

###################################################################
# START : Modules import

use strict;
use warnings;

use FindBin qw ($RealBin $Bin $Script);
use lib $RealBin, "$RealBin/ex", "$RealBin/edit";
use Storable qw (dclone thaw retrieve fd_retrieve nstore_fd);
use Expect;
use Glib::IO; # GSettings
use POSIX qw (strftime);
use Encode qw (encode decode);
use IO::Socket::INET;
use Encode;
use PACKeePass;
use PACUtils;
use POSIX qw(strftime);

use Gtk3 '-init';

binmode STDOUT;
binmode STDERR;

# END OF : Modules import
########################################################

########################################################
# START : Variables declaration/initialization
my $APPNAME = $PACUtils::APPNAME;
my $APPICON = "$RealBin/../res/asbru-logo-64.png";
my $CFG_DIR = $ENV{"ASBRU_CFG"};
my $SCRIPTS_DIR = "$CFG_DIR/scripts";

my @CMD;
my $CFG_FILE;
my $CFG;
my $IS_CLUSTER;
my $UUID;
my $GETCMD = 0;
my $CONNECTED = 0;
my $PASSWORD_COUNT = 0;
my $SUDO_PASSWORD_COUNT = 0;
my $USER_COUNT = 0;
my $HIDE_TERMINAL = 0;
my $SOCKET;
my $SOCKET_EXEC;
my $proxy_type = 'socks5';
my $proxy_ip = '';
my $proxy_port = '';
my $proxy_user = '';
my $proxy_pass = '';
my $proxy_cmd = '';
my $proxy_jump = 0;
my $proxy_get = '';
my $JUMP_PASS = 0;
my $TERM_PASS = 0;
my $PSEUDO_JUMP = 0;

my @KPXWHERE = ('comment', 'created', 'password', 'title', 'url', 'username');

# Define some ANSI colors
my %COLOR = (
    'norm' => "\033[m",    # reset
    'log' => "\033[33m",   # yellow
    'recv' => "\033[35m",  # magenta
    'cyan' => "\033[36m",  # cyan
    'sent' => "\033[0m\033[32m", # green
    'err' => "\033[31m" # red
);

# Create the Expect object
$Expect::Multiline_Matching = 1;
my $EXP = new Expect();
eval {
    $EXP->slave->clone_winsize_from(\*STDIN);
};

# END OF : Variables declaration/initialization
########################################################

########################################################
# START : Signal handling
sub __disconnect {
    my $signal = shift;
    ctrl("DISCONNECTING");
    _hardClose($EXP, $UUID);
    ctrl("DISCONNECTED");
    exit 0;
}
$SIG{'INT'} = \&__disconnect;
$SIG{'TERM'} = \&__disconnect;
$SIG{'QUIT'} = \&__disconnect;
# END OF : Signal handling
########################################################

########################################################
# START : Main program

# Retrieve command line options
$CFG_FILE = shift;
$UUID = shift or die "$COLOR{'err'}ERROR: You must provide a profile to connect to!!$COLOR{'norm'}";
$IS_CLUSTER = shift // 0;
$GETCMD = shift // 0;

# Load the 'freezed' configuration
$CFG = retrieve($CFG_FILE) or die "ERROR: Could not load config file '$CFG_FILE': $!";

# Check some command line options
defined $$CFG{'environments'}{$UUID} or die("ERROR: Profile '$UUID' does not exist!!");

my $autossh_bin = (system("$ENV{'ASBRU_ENV_FOR_EXTERNAL'} which autossh 1>/dev/null 2>&1") eq 0);

# Shorten some variables
my $DEBUG = $$CFG{'defaults'}{'debug'};
my $COMMAND_PROMPT = encode('utf8',$$CFG{'defaults'}{'command prompt'});
my $USERNAME_PROMPT = encode('utf8',$$CFG{'defaults'}{'username prompt'});
my $PASSWORD_PROMPT = encode('utf8',$$CFG{'defaults'}{'password prompt'});
my $HOSTCHANGE_PROMPT = encode('utf8',$$CFG{'defaults'}{'hostkey changed prompt'});
my $ANYKEY_PROMPT = encode('utf8',$$CFG{'defaults'}{'press any key prompt'});
my $REMOTEHOST_PROMPT = encode('utf8',$$CFG{'defaults'}{'remote host changed prompt'});
my $ACCEPT_KEY = $$CFG{'defaults'}{'auto accept key'};
my $TIMEOUT_CONNECT = $$CFG{'defaults'}{'timeout connect'} || undef;
my $TIMEOUT_CMD = $$CFG{'defaults'}{'timeout command'} || undef;
my $AUTOSSH = $$CFG{'environments'}{$UUID}{'autossh'} && $autossh_bin;
my $METHOD = $AUTOSSH ? 'autossh' : $$CFG{'environments'}{$UUID}{'method'};
my $SUDO = $$CFG{'environments'}{$UUID}{'use sudo'};
my $SUDO_PROMPT = $$CFG{'defaults'}{'sudo prompt'};
my $SUDO_PASSWORD = encode('UTF-8', $$CFG{'defaults'}{'sudo password'});
my $IP = $$CFG{'environments'}{$UUID}{'ip'};
my $PORT = $$CFG{'environments'}{$UUID}{'port'};
my $USER = $$CFG{'environments'}{$UUID}{'user'};
my $PASS = encode('UTF-8', $$CFG{'environments'}{$UUID}{'pass'});
my $AUTH = $$CFG{'environments'}{$UUID}{'auth type'};
my $PUBKEY = $$CFG{'environments'}{$UUID}{'public key'};
my $PASSPHRASE = encode('UTF-8', $$CFG{'environments'}{$UUID}{'passphrase'});
my $PASSPHRASE_USER = $$CFG{'environments'}{$UUID}{'passphrase user'};
my $AUTHFALLBACK = $$CFG{'environments'}{$UUID}{'auth fallback'};
my $NAME = $$CFG{'environments'}{$UUID}{'name'};
my $RESTART = $$CFG{'environments'}{$UUID}{'autoreconnect'} // 0;
my $SEND_SLOW = ($$CFG{'environments'}{$UUID}{'send slow'} // 0) / 1000;
my $TITLE = $$CFG{'environments'}{$UUID}{'title'};
my $EXPECT = defined $$CFG{'environments'}{$UUID}{'expect'};
my $USE_PREPEND_COMMAND = $$CFG{'environments'}{$UUID}{'use prepend command'};
my $PREPEND_COMMAND = $$CFG{'environments'}{$UUID}{'prepend command'};
my $USE_POSTPEND_COMMAND = $$CFG{'environments'}{$UUID}{'use postpend command'};
my $POSTPEND_COMMAND = $$CFG{'environments'}{$UUID}{'postpend command'};
my $QUOTE_COMMAND = $$CFG{'environments'}{$UUID}{'quote command'} && $USE_PREPEND_COMMAND;
my $QUOTEPOST_COMMAND = $$CFG{'environments'}{$UUID}{'quotepost command'} && $USE_POSTPEND_COMMAND;
my $MANUAL = ($$CFG{'environments'}{$UUID}{'auth type'} // '') eq 'manual';
my $TMP_UUID = $$CFG{'tmp'}{'uuid'};
my $LOG_FILE = $$CFG{'tmp'}{'log file'};
my $SOCK_FILE = $$CFG{'tmp'}{'socket'};
my $SOCK_EXEC_FILE = $$CFG{'tmp'}{'socket exec'};
my $REMOVE_CTRL_CHARS = $$CFG{'environments'}{$UUID}{'save session logs'} ? $$CFG{'environments'}{$UUID}{'remove control chars'} : $$CFG{'defaults'}{'remove control chars'};
my $LOG_TIMESTAMP = $$CFG{'environments'}{$UUID}{'log timestamp'} ? $$CFG{'environments'}{$UUID}{'log timestamp'} : $$CFG{'defaults'}{'log timestamp'};
my $LPORT;

if (!$RESTART && $IS_CLUSTER) {
    $RESTART = 2;
}

# Checks that session ID is still known in 'environements' ; otherwise abort immediately
if (!defined($NAME)) {
    print("$COLOR{'err'}ERROR: Session '$UUID' is invalid.$COLOR{'norm'}\n");
    exit;
}

open_socket();

# Use keepass ?
my $kpxc;
if ($$CFG{'defaults'}{'keepass'}{'use_keepass'}) {
    sub _dieMissingKeePassEntry {
        my $entry = shift;

        die("$COLOR{'err'}ERROR$COLOR{'norm'} Unable to find entry $COLOR{'log'}$entry$COLOR{'norm'} in the configured KeePass database $COLOR{'log'}$$CFG{'defaults'}{'keepass'}{database}]$COLOR{'norm'}.\n");
    }

    my $ok;
    $kpxc = PACKeePass->new(0, $$CFG{'defaults'}{'keepass'});
    if ($AUTH eq 'publickey') {
        my $ORIG_PASSPHRASE = $PASSPHRASE;
        ($PASSPHRASE, $ok) = $kpxc->getFieldValueFromString($PASSPHRASE);
        if (!$ok) {
            _dieMissingKeePassEntry($ORIG_PASSPHRASE);
        }
        my $ORIG_PASSPHRASE_USER = $PASSPHRASE_USER;
        ($PASSPHRASE_USER, $ok) = $kpxc->getFieldValueFromString($PASSPHRASE_USER);
        if (!$ok) {
            _dieMissingKeePassEntry($ORIG_PASSPHRASE_USER);
        }
    } else {
        my $ORIG_USER = $USER;
        ($USER, $ok) = $kpxc->getFieldValueFromString($USER);
        if (!$ok) {
            _dieMissingKeePassEntry($ORIG_USER);
        }
        my $ORIG_PASS = $PASS;
        ($PASS, $ok) = $kpxc->getFieldValueFromString($PASS);
        if (!$ok) {
            _dieMissingKeePassEntry($ORIG_PASS);
        }
    }
    # Makes sure the KeePass password is not exposed in the spawned process
    delete $ENV{'KPXC_MP'};
} else {
    if ($AUTH eq 'publickey') {
        $PASSPHRASE = subst($PASSPHRASE);
        $PASSPHRASE_USER = subst($PASSPHRASE_USER);
    } else {
        $USER = subst($USER);
        $PASS = subst($PASS);
    }
}

# Translate variables, KeePassXC
$TITLE = subst($TITLE,'GET_TITLE');
$IP = subst($IP);
$PORT = subst($PORT);
$$CFG{'environments'}{$UUID}{'proxy ip'}   = subst($$CFG{'environments'}{$UUID}{'proxy ip'});
$$CFG{'environments'}{$UUID}{'proxy user'} = subst($$CFG{'environments'}{$UUID}{'proxy user'});
$$CFG{'environments'}{$UUID}{'proxy pass'} = subst($$CFG{'environments'}{$UUID}{'proxy pass'});
$$CFG{'environments'}{$UUID}{'jump ip'}    = subst($$CFG{'environments'}{$UUID}{'jump ip'});
$$CFG{'environments'}{$UUID}{'jump user'}  = subst($$CFG{'environments'}{$UUID}{'jump user'});
$$CFG{'environments'}{$UUID}{'jump pass'}  = subst($$CFG{'environments'}{$UUID}{'jump pass'});
$$CFG{'defaults'}{'jump ip'}    = subst($$CFG{'defaults'}{'jump ip'});
$$CFG{'defaults'}{'jump user'}  = subst($$CFG{'defaults'}{'jump user'});
$$CFG{'defaults'}{'jump pass'}  = subst($$CFG{'defaults'}{'jump pass'});
$$CFG{'defaults'}{'proxy ip'}   = subst($$CFG{'defaults'}{'proxy ip'});
$$CFG{'defaults'}{'proxy user'} = subst($$CFG{'defaults'}{'proxy user'});
$$CFG{'defaults'}{'proxy pass'} = subst($$CFG{'defaults'}{'proxy pass'});

# Build initial SSH connection command
my $CONNECT_OPTS = $$CFG{'environments'}{$UUID}{'options'} || '';

# Test if we can avoid openning consecutive port forwarders
if ($CONNECT_OPTS =~ /(-[LR] .+?)( |$)/) {
    my $x = `$ENV{'ASBRU_ENV_FOR_EXTERNAL'} ps a | grep ssh `;
    my $y = $1;
    if ($x =~ /$y/) {
        $y =~ s/-[LR] //g;
        print "$COLOR{'log'}Skip tunnel opening $y, already open in different terminal. $COLOR{'norm'}\n\n";
        $CONNECT_OPTS =~ s/(-[LR] .+?)( |$)//g;
    }
}

my %_W;
my $INT = 0;

########################################################
# START : Procedures definition

sub open_socket {
    if ($GETCMD) {
        return 0;
    }
    $SOCKET = IO::Socket::UNIX->new(
        Type => SOCK_STREAM,
        Peer => $SOCK_FILE
    ) or die "ERROR: Could not open SOCKET file '$SOCK_FILE' for connecting: $!";
    $SOCKET->autoflush;

    if (!auth($SOCKET)) {
        die "ERROR: Service listening at file '$SOCK_FILE' is not Ásbrú";
    }

    $SOCKET_EXEC = IO::Socket::UNIX->new(
        Type => SOCK_STREAM,
        Peer => $SOCK_EXEC_FILE
    ) or die "ERROR: Could not open SOCKET file '$SOCK_EXEC_FILE' for connecting: $!";
    $SOCKET_EXEC->autoflush;
}

sub msg {
    my $msg = shift or die "$COLOR{'err'}ERROR: You must provide a message to 'msg'!!$COLOR{'norm'}";

    print "$COLOR{'log'}\[$Script($$)][$NAME][$TITLE]: $msg\r\f$COLOR{'norm'}";
    return 1;
}

sub ctrl {
    my $msg = shift or die "ERROR: You must provide a message to 'ctrl'!!";

    if ($DEBUG) {
        msg($msg);
    }
    if (!defined $SOCKET) {
        return 1;
    }

    my $wout = '';
    vec($wout, fileno($SOCKET), 1) = 1;
    select(undef, $wout, undef, 5) or die "ERROR: Could not write to PACMain SOCKET at $SOCK_FILE: $!";
    $SOCKET->send('PAC_MSG_START[' . encode('UTF-16', $msg) . ']PAC_MSG_END');
    return 1;
}

sub readData {
    my $key = shift;
    my @vals;

    while (1) {
        my @data = receiveData($SOCKET);
        foreach my $data (@data) {
            $data = decode('UTF-16', $data);
            if ($data =~ /^$key/) {
                my ($cmd,@vals) = split /\|:\|/, $data;
                return wantarray ? @vals : $vals[0];
            }
        }
    }
}

sub receiveData {
    my $socket = shift;
    my @data = ();
    my $buffer = '';
    my $data = '';
    my $bytes;

    if (!$socket) {
        die "Error, bad call to receiveData no socket!!!!";
    }
    # At least one read should be done
    do {
        $bytes = sysread($socket, $data, 1024) // 0;
        if (!defined $bytes) {
            last;
        }
        $buffer .= $data;
        chomp $buffer;
        $buffer =~ s/\R/ /go;
        my $empty_buffer = 0;
        while ($buffer =~ s/PAC_MSG_START\[(.+?)\]PAC_MSG_END/$1/o) {
            my $buffer = $1;
            if (!$buffer) {
                next;
            }
            push(@data,$buffer);
            $empty_buffer = 1;
        }
        $empty_buffer and $buffer = '';

    } until $bytes < 1024;

    return @data;
}

sub auth {
    my $socket = shift;

    &ctrl("!!_PAC_AUTH_[$$CFG{tmp}{uuid}]!!");
    my $auth = '';
    sysread($socket, $auth, 1024);
    return $auth eq "!!_PAC_AUTH_[$$CFG{tmp}{uuid}]!!";
}

sub subst {
    my $string = shift // '';
    my $get = shift;
    my ($pos);

    if (!$GETCMD && $get) {
        ctrl("$get:");
        $get = readData($get);
        return $get;
    }
    if (!defined $$CFG{'environments'}{$UUID}) {
        return $string;
    }

    $string =~ s/\<\<(proxy|jump)_host\>\>/$proxy_ip/g;
    $string =~ s/\<\<(proxy|jump)_port\>\>/$proxy_port/g;
    $string =~ s/\<\<(proxy|jump)_user\>\>/$proxy_user/g;
    $string =~ s/\<\<(proxy|jump)_pass\>\>/$proxy_pass/g;

    # Replace '<command prompt>' with user defined value for command prompt
    while ($string =~ /<command prompt>/go) {
        $string = $COMMAND_PROMPT;
    }

    # Reuse most of the implementation in PACUtils.pm
    $string = _subst($string,$CFG,$UUID,$TMP_UUID,1,$kpxc);

    # Use local wEnterValue routine
    # Replace '<ASK:#>' with user provided data for 'cmd' execution
    while ($string =~ /<ASK:(\d+?)>/go) {
        my $var = $1;
        my $val = wEnterValue("<b>Variable substitution '$var'</b>" , $string) // return undef;
        if (!defined $val) {
            last;
        }
        $string =~ s/<ASK:\Q$var\E>/$val/g;
    }

    # Replace '<ASK:*>' with user provided data for 'cmd' execution
    while ($string =~ /<ASK:(.+\|.+?)>/go) {
        my $var = $1;
        my @list;
        my $ret;
        @list = split('\|', $var);
        my $desc = shift @list;
        $var =~ s/$desc\|//;
        ($ret, $pos) = wEnterValue("<b>Choose variable value:</b>" , $desc, $var);
        defined $ret or return undef;
        $string =~ s/<ASK:(.+\|.+?)>/$ret/g;
    }

    # Replace '<ASK:*>' with user provided data for 'cmd' execution
    while ($string =~ /<ASK:(.+?)>/go) {
        my $var = $1;
        my $val = wEnterValue("<b>Variable substitution</b>" , $var) // return undef;
        if (!defined $val) {
            last;
        }
        $string =~ s/<ASK:\Q$var\E>/$val/g;
    }

    # Different from PACUtils implementation

    # Replace '<CMD:#>' with the result of executing 'cmd'
    while ($string =~ /<CMD:(.+?)>/go) {
        my $var = $1;
        my $output = `$ENV{'ASBRU_ENV_FOR_EXTERNAL'} $var 2>&1`;
        chomp $output;
        $string =~ s/<CMD:\Q$var\E>/$output/g;
    }

    return wantarray ? ($string, $pos) : $string;
}

sub wEnterValue {
    my $lblup = shift // '';
    my $lbldown = shift // '';
    my $default = shift // '';
    my $visible = shift // 1;
    my $val = '';
    my $pos = 0;

    if ($GETCMD) {
        return '&lt;ASK&gt;';
    }
    # Request a value to the PACTerminal object
    ctrl("WENTER|:|$lblup|:|$lbldown|:|$default|:|$visible");
    # Wait for data to arrive
    ($val,$pos) = readData('WENTER');
    return wantarray ? ($val, $pos) : $val;
}

sub _hideSTDERR {
    open OLDERR, ">&STDERR";
    open STDERR, ">/dev/null";
}

sub _showSTDERR {
    open STDERR, ">&OLDERR";
}

sub send_slow {
    if ($SEND_SLOW) {
        $_[0]->send_slow($SEND_SLOW, $_[1]);
    } else {
        $_[0]->send($_[1]);
    }
}

sub _getPrompt {
    # Delete any data accumulated before launching the command
    $EXP->clear_accum();
    $EXP->restart_timeout_upon_receive(1);

    send_slow($EXP, "\n");
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect(0.5, [timeout => sub {1;}], [/\R.+\R/ => sub {1;}]);
    $before_match =~ s/\R|\r|\f|\n//go;
    $before_match =~ s/(^\s*)|(\s*$)//go;
    return $before_match;
}

sub _execAndCapture {
    my $tmp = shift;

    my $pipe = $$tmp{pipe};
    my $tee = $$tmp{tee};
    my $cmd = $$tmp{cmd};
    my $pattern = $$tmp{prompt};
    my $capture = $$tmp{capture};
    my $ctrl = $$tmp{ctrl};
    my $lines = $$tmp{lines};
    my $intro = $$tmp{intro};

    $cmd = subst($cmd);
    $cmd ||= $$ctrl{cmd};

    # Delete any data accumulated before launching the command
    $EXP->clear_accum();
    $EXP->restart_timeout_upon_receive(1);

    if ($intro) {
        send_slow($EXP, $cmd . (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250') ? "\r\f" : "\n"));
    } else {
        send_slow($EXP, $cmd);
    }
    if (! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) {
        send_slow($EXP, '##__PAC__PIPE__##');
    }

    $TIMEOUT_CMD = $$CFG{'environments'}{$UUID}{'terminal options'}{'use personal settings'} ?
        ($$CFG{'environments'}{$UUID}{'terminal options'}{'timeout command'} || undef)
        :
        ($$CFG{'defaults'}{'timeout command'} || undef);

    $CONNECTED = 1;

    ctrl("PIPE_WAIT[" . ($TIMEOUT_CMD // 'indefinitely') . "][" . ((! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) ? '##__PAC__PIPE__##' : $pattern // '') . "]");

    my $ok = 0;
    # Wait for pattern prompt before continue...
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect(

        $TIMEOUT_CMD,

        [timeout => sub {ctrl("ERROR:$TIMEOUT_CMD seconds waiting for expected input");}],

        [eof => sub {
            ctrl("ERROR:Connection ended by remote peer!! " . $EXP->set_accum());
            $CONNECTED = 0;
            _hardClose($EXP, $UUID);
        }],

        [((! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) ? '##__PAC__PIPE__##' : (defined $pattern ? $pattern : '')) => sub {$ok = 1;}]
    );

    if ($ok && ($pipe || $tee || $capture || defined $ctrl)) {
       if (! defined $pattern && ($pipe || $tee || $capture || defined $ctrl)) {
           send_slow($EXP, "\x08"x17);
       }

        # Separate lines (\R matches *all kind* of new-lines: \n, \f, \r, ...)
        my @out_lines = split(/\R/, $before_match);
        # Remove last (prompt) line from output
        pop @out_lines;
        # Re-join in a single string
        my $out = join("\n", @out_lines);

        if ($capture) {
            return $out;
        } elsif (defined $ctrl) {
            $lines //= 1;
            my $tmp = join("\n", splice(@out_lines, 1, $lines));
            # Line #0 uses to be the executed command; line #1 uses to be the output of that command (one line expected!!)
            ctrl("$$ctrl{ctrl}:$tmp");
        } elsif ($tee) {
            $tee =~ /^>{1,2}(.+)$/go or $tee = '>' . $tee;
            if (!open(F,"<:utf8",$tee)) {
                return 1;
            }
            print F $out;
            close F;
        } else {
            # Advise PAC to receive command output
            ctrl("EXEC:RECEIVE_OUT");
            nstore_fd(\$out, $SOCKET_EXEC) or die "ERROR:$!";
        }
    } else {
        # Advise PAC NOT to receive command output
        ctrl("EXEC:DISCARD_OUT");
    }

    return undef;
}

sub _execScript {
    my $tmp = shift;

    if (!defined $tmp) {
        return 0;
    }

    my $name = $$tmp{name};
    my $script = $$tmp{script};
    if (!defined $script || !defined $name) {
        return 0;
    }

    our %COMMON; undef %COMMON;
    our %PAC; undef %PAC;
    our %TERMINAL; undef %TERMINAL;
    our %SHARED; undef %SHARED;

    $COMMON{subst} = sub {
        my $txt = shift // '';
        ctrl("SCRIPT_SUB_SUBST[NAME:$name][PID:$$][PARAMS:$txt]");
        return subst($txt);
    };
    $COMMON{cfg} = sub {my $ref = shift // 0; return $ref ? $CFG : dclone($CFG);};
    $COMMON{cfg_sanity} = sub {_cfgSanityCheck(shift);};
    $COMMON{del_esc} = sub {return _removeEscapeSeqs(shift // '');};

    $TERMINAL{exp} = $EXP;
    $TERMINAL{name} = $NAME;
    $TERMINAL{method} = $METHOD;
    $TERMINAL{uuid} = $UUID;
    $TERMINAL{tmp_uuid} = $TMP_UUID;
    $TERMINAL{error} = '';
    $TERMINAL{ask} = sub {
        my $txt = shift;
        my $visible = shift // 1;

        ctrl("SCRIPT_SUB_ASK[NAME:$name][PID:$$][PARAMS:$txt, $visible]");

        return wEnterValue("<b>Ásbrú SCRIPT USER INPUT</b>" , $txt, undef, $visible);
    };
    $TERMINAL{msg} = sub {msg(subst(shift));};
    $TERMINAL{log} = sub {
        my $file = shift // '';
        $file = subst($file);

        ctrl("SCRIPT_SUB_LOG[NAME:$name][PID:$$][PARAMS:$file]");

        $EXP->log_file->flush;
        $EXP->log_file($file);
        return $EXP->log_file;
    };
    $TERMINAL{send} = sub {
        my $txt = shift // '';
        $txt = subst($txt);

        ctrl("SCRIPT_SUB_SEND[NAME:$name][PID:$$][PARAMS:$txt]");

        $EXP->clear_accum();
        send_slow($EXP, $txt);
    };
    $TERMINAL{send_get} = sub {
        my $txt = shift // '';
        my $intro = shift // 1;

        ctrl("SCRIPT_SUB_SEND_GET[NAME:$name][PID:$$][PARAMS:$txt]");

        my $out = _execAndCapture({'capture' => 1, 'cmd' => $txt, 'intro' => $intro});
        if (!defined $out) {
            return undef;
        }
        $out =~ s/^.+?\R//go;
        return _removeEscapeSeqs($out);
    };
    $TERMINAL{get_prompt} = sub {
        my $del_esq = shift // 1;
        ctrl("SCRIPT_SUB_GET_PROMPT[NAME:$name][PID:$$][PARAMS:]");
        my $prompt = _getPrompt();
        if ($del_esq) {
            $prompt = _removeEscapeSeqs($prompt);
        }
        return "\Q$prompt\E";
    };
    $TERMINAL{expect} = sub {
        my $pattern = shift // '';
        my $tmout = shift // 1;

        ctrl("SCRIPT_SUB_EXPECT[NAME:$name][PID:$$][PARAMS:$pattern, $tmout]");

        $pattern = subst($pattern);
        $tmout = subst($tmout);

        $TERMINAL{out1} = undef;
        $TERMINAL{out2} = undef;
        $TERMINAL{error} = '';

        my $wait = $tmout;
        my $found = 0;
        while (!$found && $wait>0) {
            $EXP->expect(
                1,
                [eof => sub {$TERMINAL{error} = "Connection closed while waiting for pattern '$pattern'", exit 1;}],
                [$pattern => sub {
                    $TERMINAL{out1} = $EXP->before();
                    $TERMINAL{out2} = $EXP->after();
                    $TERMINAL{error} = '';
                    $found = 1;
                }]
            );
            $wait--;
        }
        if (!$found) {
            $TERMINAL{error} = "Timeout ($tmout seconds) waiting for pattern '$pattern'";
            return 0;
        }
        return $TERMINAL{error} eq '';
    };

    # Start script
    ctrl("SCRIPT_START[NAME:$name]");

    no warnings ('redefine');

    *OLDERR = *STDERR;
    open STDERR, ">/dev/null";

    eval $script;
    %SHARED = %{$$tmp{shared}};

    *STDERR = *OLDERR;

    if ($@) {
        return 0;
    }
    use warnings;

    if (!defined &CONNECTION) {
        return 1;
    }
    eval {
        local $SIG{'TERM'} = sub {
            ctrl("SCRIPT_STOPPED_MANUALLY[NAME:$name]");
            msg("Ásbrú Script '$name' terminated by user request (TERM(15) signal)");
            if (defined $_W{window}{data}){
                $_W{window}{data}->destroy;
            }
            undef %_W;
            while (Gtk3::events_pending) {
                Gtk3::main_iteration;
            }
            die;
        };

        &CONNECTION;
    };

    ctrl("SCRIPT_STOP[NAME:$name]");

    return 1;
}

sub _setProxy {
    my $method = shift;
    my $user = $USER;

    # Set variables in case they are used in function : sub subst()
    my $ssh_config = '';
    if (($$CFG{'environments'}{$UUID}{'use proxy'} == 3) || ($$CFG{'environments'}{$UUID}{'use proxy'} == 0)) {
        if ($AUTH eq 'publickey') {
            $user = $PASSPHRASE_USER;
        }
        if (-e "/etc/ssh/ssh_config") {
            open (SSHC,"<:utf8","/etc/ssh/ssh_config");
            while (my $l = <SSHC>) {
                $l =~ s/\n//g;
                $l =~ s/^[\t ]+//;
                if (!$l) {
                    next;
                } elsif ($l =~ /^#/) {
                    next;
                } elsif ($l =~ /^Host \*/) {
                    next;
                }
                $ssh_config .= "$l\n";
            }
            close SSHC;
        }
    }

    if ($$CFG{'environments'}{$UUID}{'use proxy'} == 1) {
        # Use this connection SOCKS Proxy settings
        $proxy_ip = $$CFG{'environments'}{$UUID}{'proxy ip'};
        $proxy_port = $$CFG{'environments'}{$UUID}{'proxy port'};
        $proxy_user = $$CFG{'environments'}{$UUID}{'proxy user'};
        $proxy_pass = $$CFG{'environments'}{$UUID}{'proxy pass'};
        $proxy_cmd  = _getProxyCmd($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass);
        if ($method =~ /ssh/i) {
            $CONNECT_OPTS .= " -o ProxyCommand='$proxy_cmd %h %p'";
        } else {
            _openSocksTunnel();
        }
    } elsif ($$CFG{'environments'}{$UUID}{'use proxy'} == 3) {
        # Use this connection Jump Server settings
        $proxy_jump = 1;
        $proxy_ip = $$CFG{'environments'}{$UUID}{'jump ip'};
        $proxy_port = $$CFG{'environments'}{$UUID}{'jump port'};
        $proxy_user = $$CFG{'environments'}{$UUID}{'jump user'};
        $proxy_pass = $$CFG{'environments'}{$UUID}{'jump pass'};
        open(CFG,">:utf8","$CFG_DIR/tmp/$UUID.conf");
        print CFG qq"Host *\n$ssh_config\nProtocol 2\nCompression yes\nTCPKeepAlive yes\nServerAliveInterval 60\nPreferredAuthentications publickey,hostbased,keyboard-interactive,password\n";
        print CFG qq"\nHost jumpserver\nHostName $proxy_ip\nPort $proxy_port\nUser $proxy_user\n";
        if ($$CFG{'environments'}{$UUID}{'jump key'}) {
            print CFG "IdentityFile $$CFG{'environments'}{$UUID}{'jump key'}\n";
        }
        if ($method =~ /ssh/i) {
            # Second server is only important for SSH JumpHost
            print CFG qq"\nHost server\nHostName $IP\nUser $user\nPort $PORT\n";
            if (($PUBKEY)&&($$CFG{'environments'}{$UUID}{'auth type'} eq 'publickey')) {
                print CFG "IdentityFile $PUBKEY\n";
            }
        }
        close CFG;
        if ($method =~ /VNC|RDP/i) {
            _openSshTunnel($UUID);
        }
    } elsif ($$CFG{'environments'}{$UUID}{'use proxy'} == 0) {
        # Use global configuration settings
        if ($$CFG{'defaults'}{'proxy'} eq 'Jump') {
            # Jump Server settings
            $proxy_jump = 1;
            $proxy_ip = $$CFG{'defaults'}{'jump ip'};
            $proxy_port = $$CFG{'defaults'}{'jump port'};
            $proxy_user = $$CFG{'defaults'}{'jump user'};
            $proxy_pass = $$CFG{'defaults'}{'jump pass'};
            open(CFG,">:utf8","$CFG_DIR/tmp/$UUID.conf");
            print CFG qq"Host *\n$ssh_config\nProtocol 2\nCompression yes\nTCPKeepAlive yes\nServerAliveInterval 60\nPreferredAuthentications publickey,hostbased,keyboard-interactive,password\n";
            print CFG qq"\nHost jumpserver\nHostName $proxy_ip\nPort $proxy_port\nUser $proxy_user\n";
            if ($$CFG{'defaults'}{'jump key'}) {
                print CFG "IdentityFile $$CFG{'defaults'}{'jump key'}\n";
            }
            if ($method =~ /ssh/i) {
                # Second server is only important for SSH JumpHost
                print CFG qq"\nHost server\nHostName $IP\nUser $user\nPort $PORT\n";
                if (($PUBKEY)&&($$CFG{'environments'}{$UUID}{'auth type'} eq 'publickey')) {
                    print CFG "IdentityFile $PUBKEY\n";
                }
            }
            close CFG;
            if ($method =~ /VNC|RDP/i) {
                _openSshTunnel($UUID);
            }

        } elsif ($$CFG{'defaults'}{'proxy'} eq 'Proxy') {
            # SOCKS Proxy settings
            $proxy_ip = $$CFG{'defaults'}{'proxy ip'};
            $proxy_port = $$CFG{'defaults'}{'proxy port'};
            $proxy_user = $$CFG{'defaults'}{'proxy user'};
            $proxy_pass = $$CFG{'defaults'}{'proxy pass'};
            $proxy_cmd  = _getProxyCmd($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass);
            if ($method =~ /ssh/i) {
                $CONNECT_OPTS .= " -o ProxyCommand='$proxy_cmd  %h %p'";
            } else {
                _openSocksTunnel();
            }
        }
    }
    return 0;
}

sub _getProxyCmd {
    my ($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass) = @_;

    if ($proxy_user ne '') {
        if (system("$ENV{'ASBRU_ENV_FOR_EXTERNAL'} which ncat 1>/dev/null 2>&1") eq 0) {
            return "ncat --proxy $proxy_ip:$proxy_port --proxy-type $proxy_type --proxy-auth $proxy_user:\"$proxy_pass\"";
        }
        print("$COLOR{'err'}ERROR$COLOR{'norm'}: ncat is required if using proxy user/password.\nncat is part of the nmap project (https://nmap.org/ncat/).\n");
        die();
    } else {
        return "nc -x $proxy_ip:$proxy_port";
    }
}

sub _openSshTunnel {
    my ($uuid) = @_;
    my $TMP_DIR = $ENV{"ASBRU_TMP"};

    # Get a local port that is not alreay in use
    $LPORT = _getLocalPort($PORT);

    if ($GETCMD) {
        $proxy_get .= "\nssh -F \"$CFG_DIR/tmp/$uuid.conf\" -S \"$TMP_DIR/$uuid.ctl\" -f -N -T -M -L $LPORT:$IP:$PORT jumpserver";
        return 0;
    }
    $proxy_jump = 2;

    if (-e "$TMP_DIR/$uuid.ctl") {
        # Avoid open a second terminal with the same UUID
        my $check = `$ENV{'ASBRU_ENV_FOR_EXTERNAL'} ssh -F \"$CFG_DIR/tmp/$uuid.conf\" -S \"$TMP_DIR/$uuid.ctl\" -T -O check jumpserver 2>&1`;
        if ($check =~ /=\d+/) {
            die "$COLOR{'err'}Can not duplicate VNC/RDP connections$COLOR{'norm'}\nConfigure a different second connection.\n\n";
        }
    }
    # Start SSH tunnel
    system "$ENV{'ASBRU_ENV_FOR_EXTERNAL'} ssh -F \"$CFG_DIR/tmp/$uuid.conf\" -S \"$TMP_DIR/$uuid.ctl\" -f -N -T -M -L $LPORT:$IP:$PORT jumpserver";
    $IP = '127.0.0.1';
    $PORT = $LPORT;
}

sub _openSocksTunnel {
    my $pipe = $ENV{"ASBRU_TMP"}."/pipe_$UUID";

    $LPORT = _getLocalPort($PORT);
    $proxy_cmd = _getProxyCmd($proxy_type, $proxy_ip, $proxy_port, $proxy_user, $proxy_pass);
    if ($GETCMD) {
        $proxy_get .= "\nnc -l -p $LPORT <$pipe | $proxy_cmd $IP $PORT >$pipe &";
        return 0;
    }

    unlink($pipe);
    system("$ENV{'ASBRU_ENV_FOR_EXTERNAL'} mkfifo $pipe");
    system("$ENV{'ASBRU_ENV_FOR_EXTERNAL'} nc -l -p $LPORT <$pipe | $proxy_cmd $IP $PORT >$pipe &");
    $IP = '127.0.0.1';
    $PORT = $LPORT;
}

# Find out a free local TCP port
sub _getLocalPort {
    my $LPORT = shift;

    my $PING = Net::Ping->new('tcp');
    $PING->service_check(0);

    for (my $break = 0; $break < 100; ++$break) {
        $PING->port_number($LPORT);
        if (!$PING->ping('localhost')) {
            return $LPORT;
        }
        $LPORT++;
    }


    return $LPORT;
}

# We have a jump proxy, we have to figure out which password is being asked: Jump server? Destination?

sub getJumpPassword {
    my $match = $EXP->match();
    my $debug = $DEBUG ? $DEBUG : $ENV{'VERBOSE'};

    if ($debug) {
        print "\n$COLOR{log}",
        "MATCH   : >$match<\n",
        "IP      : $IP\n",
        "JUMP IP : $proxy_ip\n",
        "DEFAULT  KEY : $$CFG{'defaults'}{'jump key'}\n",
        "TERMINAL KEY : $$CFG{'environments'}{$UUID}{'jump key'}\n",
        "$COLOR{'norm'}";
    }

    if ($$CFG{'environments'}{$UUID}{'use proxy'} == 3 && !$JUMP_PASS) {
        # Use terminal configuration settings
        if ($$CFG{'environments'}{$UUID}{'jump key'} && $match =~ /$$CFG{'environments'}{$UUID}{'jump key'}/) {
            $JUMP_PASS++;
            if ($proxy_pass) {
                if ($debug) {
                    print "$COLOR{cyan}TRY JUMP UUID PASSPRHASE$COLOR{'norm'}\n";
                }
                return $proxy_pass;
            }
            if ($debug) {
                print "$COLOR{recv}* ASK JUMP UUID PASSPRHASE$COLOR{'norm'}\n";
            }
            return askPassword();
        }
    } elsif ($$CFG{'environments'}{$UUID}{'use proxy'} == 0 && $$CFG{'defaults'}{'proxy'} eq 'Jump' && !$JUMP_PASS) {
        # Use global configuration settings
        if ($$CFG{'defaults'}{'jump key'} && $match =~ /$$CFG{'defaults'}{'jump key'}/) {
            $JUMP_PASS++;
            if ($proxy_pass) {
                if ($debug) {
                    print "$COLOR{cyan}TRY JUMP DEFAULT PASSPHRASE OK!$COLOR{'norm'}\n";
                }
                return $proxy_pass;
            }
            if ($debug) {
                print "$COLOR{recv}* ASK JUMP DEFAULT PASSPRHASE$COLOR{'norm'}\n";
            }
            return askPassword();
        }
    } elsif ($AUTH eq 'publickey' && $PUBKEY && $match =~ /$PUBKEY/ && $TERM_PASS<2) {
        $TERM_PASS++;
        if ($PASSPHRASE) {
            if ($debug) {
                print "$COLOR{cyan}TRY TERMINAL PASSPHRASE ($TERM_PASS)$COLOR{'norm'}\n";
            }
            return $PASSPHRASE;
        }
        if ($debug) {
            print "$COLOR{recv}* ASK TERMINAL PASSPHRASE ($TERM_PASS)$COLOR{'norm'}\n";
        }
        return askPassword();
    }
    if ($debug) {
        print "$COLOR{log}",
        "JUMP PASS? $JUMP_PASS\n",
        "TERM PASS? $TERM_PASS",
        "$COLOR{'norm'}\n";
    }
    if (!$JUMP_PASS && $proxy_pass) {
        if ($debug) {
            print "$COLOR{cyan}TRY DEFAULT JUMP$COLOR{'norm'}\n";
        }
        $JUMP_PASS++;
        return $proxy_pass;
    }
    $JUMP_PASS++;
    if (!$TERM_PASS) {
        # Try a terminal password, maybe the jump server is alrealy logged in
        $TERM_PASS++;
        if ($AUTH eq 'publickey' && $PASSPHRASE) {
            if ($debug) {
                print "$COLOR{cyan}TRY DESTINATION PASSPHRASE$COLOR{'norm'}\n";
            }
            return $PASSPHRASE;
        } elsif ($AUTH eq 'userpass' && $PASS && $PASS ne '<<ASK_PASS>>') {
            if ($debug) {
                print "$COLOR{cyan}TRY DESTINATION PASSWORD$COLOR{'norm'}\n";
            }
            return $PASS;
        }
    }
    if ($debug) {
        print "$COLOR{recv}* ASK PASSWORD$COLOR{'norm'}\n";
    }
    # Extract the last line and use it is as prompt for the request password dialog
    return askPassword();
}

sub askPassword {
    my @before;

    my $real_prompt = (scalar @before > 0 ? $before[-1] : '') . $EXP->match();
    ctrl("PASSWORD:Asking user for password...");
    my $p = wEnterValue('<b>Manual Password requested</b>', $real_prompt, '', 0);
    return $p;
}

# Place any thing that has to be done before a hard_close; and finally do the hard_close
sub _hardClose {
    my ($EXP, $uuid)  = @_;
    my $pipe = $ENV{"ASBRU_TMP"}."/pipe_$uuid";
    my $sock = $ENV{"ASBRU_TMP"}."/$uuid.ctl";
    my $cmd_file = "$CFG_DIR/tmp/cmd_$uuid.run";

    if (-e $cmd_file) {
        unlink($cmd_file);
    }

    if ($proxy_jump == 2 && -e $sock) {
        system "$ENV{'ASBRU_ENV_FOR_EXTERNAL'} ssh -F \"$CFG_DIR/tmp/$uuid.conf\" -S \"$sock\" -T -O exit jumpserver";
    }
    if (-e $pipe) {
        unlink($pipe);
    }
    if ($IS_CLUSTER && ($RESTART == 2)) {
        ctrl('RESTART');
    }
    if ($EXP) {
        $EXP->hard_close();
    }
    return 0;
}

sub logTime {
    my $t = encode('utf8',strftime "%Y-%m-%d %H:%M:%S", localtime());
    print LOG "$t : $_[0]";
    if ($_[0] !~ /\n|\r$/) {
        print LOG "\n";
    }
}

# If we want a perfect fit of an embedded RDP tab, we can use X11::GUITest if available
# We should eval in BEGIN or perl warns "Too late to run INIT block"
my $module_guitest;
BEGIN{
    eval("use X11::GUITest qw (GetWindowPos GetWindowName)");
    $module_guitest = $@ ? "N" : "Y";
}
# END OF : Procedures definition
########################################################

# Prepare Networking

# Choose and prepare the connection method
my $connection_cmd = '';
my $connection_cmd2 = '';
my $connection_txt = '';

if (defined $METHOD) {
    ##############################################
    # TERMINAL METHODS (ssh, telnet, etc)
    ##############################################
    if (($METHOD =~ /^.*ssh.*$/) || ($METHOD eq 'SSH')) {
        _setProxy('ssh');

        if (defined $$CFG{'tmp'}{'randomSocksTunnel'}) {
          $CONNECT_OPTS .= ' -D localhost:' . $$CFG{'tmp'}{'randomSocksTunnel'};
        }

        if ($METHOD ne 'autossh') {
            $METHOD = 'ssh';
        }
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = ($PUBKEY ? "-i \"$PUBKEY\"" : "") . ($AUTHFALLBACK ? '' : ' -o "PreferredAuthentications=publickey"');
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
        } elsif (! $AUTHFALLBACK) {
            $key = '-o "PreferredAuthentications=password,keyboard-interactive"';
        }
        # Build the command line
        $connection_cmd = $METHOD
                          . ($PORT > 0 ? " -p $PORT" : '')
                          . ($key ne '' ? " $key" : "")
                          . $CONNECT_OPTS
                          . ($USER ? " -l $USER " : '')
                          . " $IP";
        if ($proxy_jump) {
            if ($$CFG{'environments'}{$UUID}{'pseudo jump'}) {
                # Pseudo jump, connect to the first server and then we will send the second connection command over the second server
                $connection_cmd2 = $connection_cmd;
                $connection_cmd = "$METHOD -F \"$CFG_DIR/tmp/$UUID.conf\" $CONNECT_OPTS jumpserver";
            } else {
                $connection_cmd = "$METHOD -F \"$CFG_DIR/tmp/$UUID.conf\" $CONNECT_OPTS -J jumpserver server";
            }
        }
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*mosh.*$/) || ($METHOD eq 'MOSH')) {
        my $key = '';
        $METHOD = 'mosh';
        if ($AUTH eq 'publickey') {
            $key = $PUBKEY ? "-i '$PUBKEY'" : '';
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
            $CONNECT_OPTS .= " --ssh=\"ssh -p $PORT $key\"";
        } elsif ($PORT != 22) {
            $CONNECT_OPTS .= " --ssh=\"ssh -p $PORT\"";
        }
        $connection_cmd = "$METHOD $CONNECT_OPTS ${USER}\@$IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*sftp.*$/) || ($METHOD eq 'SFTP')) {
        _setProxy('ssh');
        $METHOD = 'sftp';
        my $key = '';
        if ($AUTH eq 'publickey') {
            $key = ($PUBKEY ? "-i \"$PUBKEY\"" : "") . ($AUTHFALLBACK ? '' : ' -o "PreferredAuthentications=publickey"');
            $USER = $PASSPHRASE_USER;
            $PASS = $PASSPHRASE;
        } elsif (! $AUTHFALLBACK) {
            $key = '-o "PreferredAuthentications=password,keyboard-interactive"';
        }
        if ($proxy_jump) {
            $connection_cmd = "$METHOD -F \"$CFG_DIR/tmp/$UUID.conf\" $CONNECT_OPTS -o Proxyjump=jumpserver server";
        } else {
            $connection_cmd = "$METHOD $key -P $PORT $CONNECT_OPTS " . ($USER ? "$USER@" : '') . "$IP";
        }
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*telnet.*$/) || ($METHOD eq 'Telnet')) {
        $METHOD = 'telnet';
        my $port = ($PORT == 23) ? '' : $PORT;
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP $port";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*ftp*$/) || ($METHOD eq 'FTP')) {
        $METHOD = 'ftp';
        my $port = ($PORT == 21) ? '' : $PORT;
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP $port";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*cadaver*$/) || ($METHOD eq 'WebDAV')) {
        $METHOD = 'cadaver';
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*cu$/) || ($METHOD eq 'Serial (cu)')) {
        $METHOD = 'cu';
        $connection_cmd = "$METHOD $CONNECT_OPTS $IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*remote-tty$/) || ($METHOD eq 'Serial (remote-tty)')) {
        $METHOD = 'remote-tty';
        $connection_cmd = "$METHOD $CONNECT_OPTS" . ($USER ? " -l $USER" : '') . " $IP";
        $connection_txt = $connection_cmd;
    } elsif (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) {
        $METHOD = 'c3270';
        $CONNECT_OPTS =~ s/\s+-prepend_([P|S|N|L])//go;
        my $modifier = $1;
        $connection_cmd = "$METHOD $CONNECT_OPTS " . ($modifier ? "$modifier:" : '') . "[$IP]:$PORT";
        $connection_txt = $connection_cmd;
    }
    ##############################################
    # TERMINAL DESKTOP METHODS (rdp, vnc, etc)
    ##############################################
    elsif ($METHOD =~ /^.*RDP \((.+)\).*$/) {
        $HIDE_TERMINAL = 1;
        $METHOD = $1;
        _setProxy('RDP');
        if ((defined $$CFG{'tmp'}{'xid'}) && ($METHOD eq 'rdesktop')) {
            if ($module_guitest eq 'Y') {
                my ($xpos, $ypos, $Xwidth, $Xheight, $borderWidth, $_screen) = GetWindowPos($$CFG{'tmp'}{'xid'});
                if ($Xwidth > 3 && $Xheight > 1) {
                    $$CFG{'tmp'}{'width'} = $Xwidth - 2;
                    $$CFG{'tmp'}{'height'} = $Xheight;
                }
            }
            $connection_cmd = "$METHOD -X $$CFG{'tmp'}{'xid'} -g $$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " $IP:$PORT";
            $connection_txt = "$METHOD -X $$CFG{'tmp'}{'xid'} -g $$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " $IP:$PORT";
        } elsif ((defined $$CFG{'tmp'}{'xid'}) && ($METHOD =~ /^.*freerdp$/)) {
            if ($module_guitest eq 'Y') {
                my ($xpos, $ypos, $Xwidth, $Xheight, $borderWidth, $_screen) = GetWindowPos($$CFG{'tmp'}{'xid'});
                my $name = GetWindowName($$CFG{'tmp'}{'xid'});
                if ($Xwidth > 1 && $Xheight > 1) {
                    $$CFG{'tmp'}{'width'} = $Xwidth;
                    $$CFG{'tmp'}{'height'} = $Xheight;
                }
            }
            # Just in case we end up with impossible values
            if ($$CFG{'tmp'}{'width'} <= 1) {
                $$CFG{'tmp'}{'width'} = 1024;
            }
            if ($$CFG{'tmp'}{'height'} <= 1) {
                $$CFG{'tmp'}{'height'} = 768;
            }
            $connection_cmd = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'$PASS'") . " /v:$IP:$PORT";
            $connection_txt = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'<<hidden_password>>'") . " /v:$IP:$PORT";
        } elsif (defined $$CFG{'tmp'}{'xid'}) {
            $connection_cmd = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'$PASS'") . " /v:$IP:$PORT";
            $connection_txt = "$METHOD /parent-window:$$CFG{'tmp'}{'xid'} /size:$$CFG{'tmp'}{'width'}x$$CFG{'tmp'}{'height'} $CONNECT_OPTS" . ($MANUAL ? '' : " /u:'$USER' /p:'<<hidden_password>>'") . " /v:$IP:$PORT";
        } elsif ($METHOD eq 'rdesktop') {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " -T \"$TITLE\" $IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER -p -") . " -T \"$TITLE\" $IP:$PORT";
        } elsif (($METHOD =~ /^.*freerdp$/) && (defined $PASS)) {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER /p:'$PASS'") . " /t:\"$TITLE\" /v:$IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER /p:'<<hidden_password>>'") . " /t:\"$TITLE\" /v:$IP:$PORT";
        } elsif ($METHOD =~ /^.*freerdp$/) {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER") . " /t:\"$TITLE\" /v:$IP:$PORT";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " /u:$USER") . " /t:\"$TITLE\" /v:$IP:$PORT";
        } else {
            $connection_cmd = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER") . " $IP:$PORT -T \"$TITLE\"";
            $connection_txt = "$METHOD $CONNECT_OPTS" . ($MANUAL ? '' : " -u $USER") . " $IP:$PORT -T \"$TITLE\"";
        }
    }
    elsif (($METHOD =~ /^.*vncviewer$/) || ($METHOD eq 'VNC')) {
        $METHOD = 'vncviewer';
        if (($PASS eq '<<ASK_PASS>>') || ($MANUAL)) {
            ctrl("PASSWORD:Asking user for password...");
            $PASS = wEnterValue('<b>Manual Password requested</b>', "Enter Password for '$NAME'", '', 0) // '';
        }
        my $method = 'VNC';
        my $vncviewer_cmd = "LC_ALL=C $METHOD";
        $CONNECT_OPTS =~ s/\s*-embed//go;
        if (`$ENV{'ASBRU_ENV_FOR_EXTERNAL'} vncviewer --help 2>&1 | $ENV{'ASBRU_ENV_FOR_EXTERNAL'} /bin/grep RealVNC`) {
            $method = 'RealVNC';
        } elsif (`$ENV{'ASBRU_ENV_FOR_EXTERNAL'} vncviewer --help 2>&1 | $ENV{'ASBRU_ENV_FOR_EXTERNAL'} /bin/grep TigerVNC`) {
            $method = 'TigerVNC';
        }
        _setProxy($method);
        if ($method eq 'TigerVNC') {
            my $pfile = "$CFG_DIR/tmp/asbru_conn_{$$}_$UUID";
            system("$ENV{'ASBRU_ENV_FOR_EXTERNAL'} echo \"" . _doShellEscape($PASS) . "\" | vncpasswd -f > \"$pfile\"");
            if (defined $$CFG{'tmp'}{'xid'}) {
                $connection_cmd = "$vncviewer_cmd $CONNECT_OPTS -PasswordFile=\"$pfile\" -Parent=$$CFG{'tmp'}{'xid'} $IP:$PORT";
                $connection_txt = "$vncviewer_cmd $CONNECT_OPTS -PasswordFile=\"$pfile\" -Parent=$$CFG{'tmp'}{'xid'} $IP:$PORT";
            } else {
                $connection_cmd = "$vncviewer_cmd $CONNECT_OPTS -PasswordFile=\"$pfile\" $IP:$PORT";
                $connection_txt = "$vncviewer_cmd $CONNECT_OPTS -PasswordFile=\"$pfile\" $IP:$PORT";
            }
        } elsif ($method eq 'RealVNC') {
            my $user = $USER ? "UserName=$USER" : '';

            $connection_cmd = "$vncviewer_cmd $CONNECT_OPTS $user $IP:$PORT";
            $connection_txt = "$vncviewer_cmd $CONNECT_OPTS $user $IP:$PORT";
            $METHOD = "Real$METHOD";
        } else {
            my $user = '';

            $connection_cmd = "echo \"" . _doShellEscape($PASS) . "\" | $vncviewer_cmd $CONNECT_OPTS $user $IP:$PORT";
            $connection_txt = "echo \"<<hidden_password>>\" | $vncviewer_cmd $CONNECT_OPTS $user $IP:$PORT";
        }
    }
    ##############################################
    # GENERIC METHOD (simple command line)
    ##############################################
    elsif (($METHOD =~ /^.*generic$/) || ($METHOD eq 'Generic Command')) {
        $connection_cmd = "$IP";
        $connection_txt = "$IP";
    }
    elsif ($METHOD eq 'PACShell') {
        $connection_cmd = "$$CFG{'defaults'}{'shell binary'} $$CFG{'defaults'}{'shell options'}";
        $connection_txt = "$$CFG{'defaults'}{'shell binary'} $$CFG{'defaults'}{'shell options'}";
    }
    ##############################################
    # UNKNOWN METHOD (error!)
    ##############################################
    else {
        my $string = "Unsupported connection method '$METHOD'.";
        msg($string);
        ctrl("ERROR:$string");
        ctrl("DISCONNECTED");
        exit 1;
    }
}

# Check for prepend commands
if ($QUOTE_COMMAND) {
    $connection_cmd = qq|"$connection_cmd"|;
}
if ($QUOTE_COMMAND) {
    $connection_txt = qq|"$connection_cmd"|;
}
if ($QUOTEPOST_COMMAND) {
    $connection_cmd = qq|"$connection_cmd"|;
}
if ($QUOTEPOST_COMMAND) {
    $connection_txt = qq|"$connection_cmd"|;
}
if ($USE_PREPEND_COMMAND) {
    $connection_cmd = "$PREPEND_COMMAND $connection_cmd";
}
if ($USE_PREPEND_COMMAND) {
    $connection_txt = "$PREPEND_COMMAND $connection_txt";
}
if ($USE_POSTPEND_COMMAND) {
    $connection_cmd = "$connection_cmd $POSTPEND_COMMAND ";
}
if ($USE_POSTPEND_COMMAND) {
    $connection_txt = "$connection_txt $POSTPEND_COMMAND ";
}

# Check for 'sudo' use
if ($SUDO) {
    $connection_cmd = "sudo -p '$SUDO_PROMPT' $connection_cmd";
    $connection_txt = "sudo -p '$SUDO_PROMPT' $connection_txt";
}

# Check if there are non-generic terminal settings
if ($$CFG{'environments'}{$UUID}{'terminal options'}{'use personal settings'}) {
    $TIMEOUT_CONNECT = $$CFG{'environments'}{$UUID}{'terminal options'}{'timeout connect'} || undef;
    $TIMEOUT_CMD = $$CFG{'environments'}{$UUID}{'terminal options'}{'timeout command'} || undef;
    $COMMAND_PROMPT = $$CFG{'environments'}{$UUID}{'terminal options'}{'command prompt'};
    $USERNAME_PROMPT = $$CFG{'environments'}{$UUID}{'terminal options'}{'username prompt'};
    $PASSWORD_PROMPT = $$CFG{'environments'}{$UUID}{'terminal options'}{'password prompt'};
}

$connection_cmd = subst($connection_cmd);
if ($GETCMD) {
    print "<b>Command line:</b>\n";
    print Glib::Markup::escape_text($connection_cmd);
    if ($proxy_get ne '') {
        print "\n\n<b>Proxy setup:</b>";
        print Glib::Markup::escape_text($proxy_get);
    }
    exit 0;
}

# Set log file
# Only if the target directory is writable; if not writable, we can silently ignore the erroneous log file since the warning has already been raised in the main application

if (open(LOG,">:raw",$LOG_FILE)) {
    if ($LOG_TIMESTAMP) {
        $EXP->log_file(\&logTime);
    } else {
        close LOG;
        $EXP->log_file($LOG_FILE);
    }
} else {
    print "\n$COLOR{'err'}ERROR: could not open log file :$LOG_FILE$COLOR{'norm'}\n(check path and permissions)\n\n";
}

# Prepare command file that is only accessible to the user
my $do_connection_cmd = $connection_cmd;
my $cmd_file = "$CFG_DIR/tmp/cmd_${UUID}_" . int(rand(123456789)) . ".run";
if ($METHOD ne 'PACShell') {
    if (open(my $CMD, ">:utf8", $cmd_file)) {
        chmod(0400, $CMD);
        print { $CMD } "rm -f \"$cmd_file\"\n"; # deleting the temporary file immediately after it has been started
        print { $CMD } $connection_cmd;
        close($CMD);
        $do_connection_cmd = "sh \"$cmd_file\"";
    }
}

# Spawn the session
ctrl("SPAWNING:$connection_txt");
my $external_dir = $ENV{'ASBRU_SUB_CWD'} // '.';
$do_connection_cmd = "$ENV{'ASBRU_ENV_FOR_EXTERNAL'} cd '$external_dir' && $ENV{'ASBRU_ENV_FOR_EXTERNAL'} $do_connection_cmd";
$EXP->spawn("$do_connection_cmd 2>&1") or die "Cannot spawn '$connection_cmd: $!\n";
ctrl("SPAWNED:'$connection_txt' (PID:$$)");

$EXP->exp_internal($DEBUG); # EXPECT DEBUG!!

if (($METHOD =~ /^.*rdesktop$/) || ($METHOD =~ /^.*freerdp.*/) || ($METHOD eq 'RDP (Windows)')) {
    $CONNECTED = 1;
    ctrl("CONNECTED");

    $EXP->expect($TIMEOUT_CONNECT,

        [eof => sub {
            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
            _hardClose($EXP, $UUID);
        }],

        ['\[ERROR\]', sub {
        }],

        # Found certificate verification string
        ['^.+WARNING: CERTIFICATE NAME MISMATCH!.+$', sub {
            if ($ACCEPT_KEY) {
                send_slow($EXP, "Y\n");
                ctrl("HOSTKEY:accepted by configuration (sent 'Y')");
                exp_continue();
            } else {
                $CONNECTED = 0;
                send_slow($EXP, "N\n");
                ctrl("CLOSE:HOSTKEY:rejected by configuration (sent 'N')");
            }
        }],

        # Found question about untrusted certificate ; keep the terminal open and
        # let the user review this manually
        ['Do you trust this certificate', sub {
            $HIDE_TERMINAL = 0;
        }],

        # Found login string
        [$USERNAME_PROMPT, sub {
            if (($USER eq '') && ($USER_COUNT)) {
                ctrl("CLOSE:LOGIN:Empty username is not valid to login here");
                _hardClose($EXP, $UUID);
            }
            $USER_COUNT++;
            $USER = subst($USER);
            ctrl("LOGIN:$USER");
            send_slow($EXP, "$USER\n");
            exp_continue();
        }],

        # Found password/passphrase string
        [$PASSWORD_PROMPT, sub {
            # First password attempt...
            if (! $PASSWORD_COUNT) {
                $PASSWORD_COUNT++;
                if (($PASS eq '<<ASK_PASS>>') || ($MANUAL)) {
                    ctrl("PASSWORD:Asking user for password...");
                    $PASS = wEnterValue('<b>Manual Password requested</b>', "Please, enter Password", '', 0);
                }
                if (defined $PASS) {
                    $EXP->log_stdout(0);
                    $PASS = subst($PASS);
                    send_slow($EXP, "$PASS\n", 'hide');
                    $EXP->log_stdout(1);
                    ctrl("PASSWORD:Sent (not shown)");
                    exp_continue();
                } else {
                    ctrl("CLOSE:PASSWORD:Password input cancelled by user");
                    $CONNECTED = 0;
                    _hardClose($EXP, $UUID);
                }
            }
            # ... second password attempt: provided password is no longer valid!!
            else {
                ctrl("CLOSE:PASSWORD:Provided username/password '$USER/<<hidden_password>>' was rejected");
                _hardClose($EXP, $UUID);
            }
        }],

        # Found success string for xfreerdp
        ['\[INFO\]', sub {
            return 1;
        }],
    )
}
elsif (($METHOD =~ /^.*vncviewer$/) || ($METHOD eq 'VNC')) {
    # Expect authentication confirmation...
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect($TIMEOUT_CONNECT,
        [timeout => sub {
            if ($METHOD =~ /Real/) {
                # Special case for RealVNC, no information to control timeout, logins, etc.
                $CONNECTED = 1;
                ctrl("CONNECTED");
            } else {
                $CONNECTED = 0;
                ctrl("CLOSE:TIMEOUT:$TIMEOUT_CONNECT seconds trying to connect or get prompt!!");
                _hardClose($EXP, $UUID);
            }
        }],

        [eof => sub {

            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
            _hardClose($EXP, $UUID);
        }],

        # Found login string
        ['^.*Authentication successful.*$', sub {
            $CONNECTED = 1;
            ctrl("CONNECTED");
        }],

        # No auth is needed
        ['^.*No authentication needed*$', sub {
            $CONNECTED = 1;
            ctrl("CONNECTED");
        }],

        # Found login string
        ['^.*Conn:\s+[cC]onnected to host\s+.+\s+port\s+\d+.*$', sub {
            $CONNECTED = 1;
            ctrl("CONNECTED");
        }],

        # Found login string
        [$USERNAME_PROMPT, sub {

            if (($USER eq '') && ($USER_COUNT)) {
                ctrl("CLOSE:LOGIN:Empty username is not valid to login here");
                _hardClose($EXP, $UUID);
            }
            $USER_COUNT++;
            $USER = subst($USER);
            ctrl("LOGIN:$USER");
            send_slow($EXP, "$USER\n");
            exp_continue();
        }],

        # Found password/passphrase string
        [$PASSWORD_PROMPT, sub {

            my $user = $EXP->before // '';
            $user =~ s/^(.+?)@.+/$1/go;

            ctrl("PASSWORD:Asking user for gateway's password...");
            $PASS = wEnterValue("<b>Gateway Password requested</b>", "Enter Password for gateway's user '$user'", '', 0);

            if (defined $PASS) {
                $EXP->log_stdout(0);
                $PASS = subst($PASS);
                send_slow($EXP, "$PASS\n", 'hide');
                $EXP->log_stdout(1);
                ctrl("PASSWORD:Sent (not shown)");
                exp_continue();
            } else {
                ctrl("CLOSE:PASSWORD:Password input cancelled by user");
                _hardClose($EXP, $UUID);
            }
        }],

        # Found any other string (special case for the '-via' option)
        ['.*((open|connect) failed)|refused|(server closed).*', sub {
            ctrl("DISCONNECTED");
            $CONNECTED = 0;
        }],

    )
}
elsif (($METHOD =~ /^.*generic$/) || ($METHOD eq 'Generic Command') || ($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) {
    $CONNECTED = 1;

    # Do we have to do complex expects?
    if ((!$EXPECT || !$CONNECTED)) {
        return 1;
    }

    my $end = 0;
    my $avoid_first_expectation = 0;
    $EXP->restart_timeout_upon_receive(1);
    for(my $i = 0; $i < scalar(@{$$CFG{'environments'}{$UUID}{'expect'}}); $i++) {
        my $hash = $$CFG{'environments'}{$UUID}{'expect'}[$i];
        my $pattern = $$hash{'expect'} // '';
        my $command = $$hash{'send'} // '';
        my $hide = $$hash{'hidden'} // 0;
        my $active = $$hash{'active'} // 0;
        my $return = $$hash{'return'} // 1;
        my $on_match = $$hash{'on_match'} // -1;
        my $on_fail = $$hash{'on_fail'} // -1;
        my $time_out = $$hash{'time_out'} // -1;

        if (!$active) {
            next;
        }
        my $skip_command = 0;

        if ($time_out >= 0) {
            $TIMEOUT_CMD = $time_out;
        }

        $pattern = subst($pattern);

        ctrl("EXPECT:WAITING:$pattern");

        # Wait for pattern prompt before continue...
        $EXP->expect($TIMEOUT_CMD,

            [timeout => sub {
                if ($on_fail == -1) {
                    ctrl("CLOSE:TIMEOUT:$TIMEOUT_CMD seconds expecting pattern '$pattern'!!");
                    $CONNECTED = 0;
                    _hardClose($EXP, $UUID);
                } elsif ($on_fail == -2) {
                    ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Finishing Expect");
                    $CONNECTED = 1;
                    $end = 1;
                } else {
                    ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Jumping to '$on_fail'");
                    $i = --$on_fail;
                    $skip_command = 1;
                }
            }],

            [eof => sub {

                $CONNECTED = 0;
                ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
                _hardClose($EXP, $UUID);
            }],

            [($avoid_first_expectation) ? '' : $pattern, sub {
                if ($on_match == -1) {
                    $skip_command = 0;
                } elsif ($on_match == -2) {
                    $end = 1;
                    $CONNECTED = 1;
                    ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Stopping by config");
                } else {
                    ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Jumping to '$on_match'");
                    $avoid_first_expectation = 1;
                    $i = --$on_match;
                }
            }]
        );
        last if $end;
        next if $skip_command;
        $avoid_first_expectation = 0;
        if (!$CONNECTED) {
            last;
        }

        $command = subst($command);

        # ... and launch command
        if ($hide) {
            ctrl("EXPECT:SENDING:<<HIDDEN STRING>>");
            $EXP->log_stdout(0);
        } else {
            my $cmd_str = $command;
            $cmd_str =~ s/\n$//go;
            ctrl("EXPECT:SENDING:$cmd_str" . ($return ? '\n' : ''));
        }

        send_slow($EXP, $command . ($return ? (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250') ? "\r\f" : "\n") : ''));
        if ($hide) {
            $EXP->log_stdout(1);
        }
    }

    $EXP->restart_timeout_upon_receive(0);

    if ($CONNECTED) {
        ctrl("TITLE:$TITLE");
        ctrl("CONNECTED");
    } else {
        $CONNECTED = 0;
        ctrl("DISCONNECTED:" . ($EXP->error));
    }

}
elsif ($METHOD eq 'PACShell') {
    $CONNECTED = 1;
    ctrl("CONNECTED");
}
elsif (!$MANUAL) {
    my $end = 0;

    # Expect password/confirmation prompt...
    my ($matched_pattern_position, $error, $successfully_matching_string, $before_match, $after_match) = $EXP->expect($TIMEOUT_CONNECT,

        [timeout => sub {
            $CONNECTED = 0;
            ctrl("CLOSE:TIMEOUT:$TIMEOUT_CONNECT seconds trying to connect or get prompt!!");
            _hardClose($EXP, $UUID);
        }],

        [eof => sub {
            $CONNECTED = 0;
            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
            _hardClose($EXP, $UUID);
        }],

        # Found sudo prompt
        ["\Q$SUDO_PROMPT\E", sub {
            if ($CONNECTED) {
                $EXP->exp_continue();
            }
            # First 'sudo' password attempt...
            if (! $SUDO_PASSWORD_COUNT) {
                $SUDO_PASSWORD_COUNT++;
                if ((defined $SUDO_PASSWORD) && ($SUDO_PASSWORD eq '<<ASK_PASS>>')) {
                    ctrl("PASSWORD:Asking user '$ENV{'USER'}' for 'sudo' password...");
                    $SUDO_PASSWORD = wEnterValue('<b>Password requested</b>', "Enter 'sudo' password for '$ENV{'USER'}'", '', 0);
                }
                if (defined $SUDO_PASSWORD) {
                    $EXP->log_stdout(0);
                    $SUDO_PASSWORD = subst($SUDO_PASSWORD);
                    send_slow($EXP, "$SUDO_PASSWORD\n", 'hide');
                    $EXP->log_stdout(1);
                    ctrl("PASSWORD:'sudo' password sent (not shown)");
                    exp_continue();
                } else {
                    ctrl("CLOSE:PASSWORD:Password for 'sudo' input cancelled by user");
                    _hardClose($EXP, $UUID);
                }
            }
            # ... second 'sudo' password attempt: provided password is no longer valid!!
            else {
                ctrl("CLOSE:PASSWORD:Provided 'sudo' password was rejected");
                _hardClose($EXP, $UUID);
            }
        }],

        # Found Host-Key verification string
        [$HOSTCHANGE_PROMPT, sub {
            my $match = $EXP->match();
            $match =~ /$HOSTCHANGE_PROMPT/g;
            my ($yes, $no) = ($1, $2);
            if ($ACCEPT_KEY) {
                send_slow($EXP, "$yes\n");
                ctrl("HOSTKEY:accepted by configuration (sent '$yes')");
                exp_continue();
            } else {
                my $val = wEnterValue($match, "Answer yes/no") // return undef;
                send_slow($EXP, "$val\n");
                ctrl("HOSTKEY:user value sent '$val')");
                exp_continue();
            }
        }],

        # Found "Press any key to continue" string
        [$ANYKEY_PROMPT, sub {
            send_slow($EXP, "\n");
            ctrl("PRESSKEY:sending 'return' to continue connecting");
            exp_continue();
        }],

        # Found WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED
        [$REMOTEHOST_PROMPT, sub {
            my $match = $EXP->match();
            chomp $match;
            $match =~ /$REMOTEHOST_PROMPT/go;
            my ($known_hosts, $line) = ($1, $2);
            $CONNECTED = 0;
            if ($ACCEPT_KEY) {
                ctrl("HOSTKEY:deleting offending hostkey by configuration and Restarting connection");
                open(F, `$ENV{'ASBRU_ENV_FOR_EXTERNAL'} echo $known_hosts 2>&1`) or die "ERROR: could not open for reading '$known_hosts' ($!)";
                my @data = <F>;
                close F;
                splice(@data, $line - 1, 1);
                open(F, '>' . `$ENV{'ASBRU_ENV_FOR_EXTERNAL'} echo $known_hosts 2>&1`) or die "ERROR: could not open for writing '$known_hosts' ($!)";
                print F @data;
                close F;
                ctrl('RESTART');
            } else {
                ctrl("CLOSE:HOSTKEY:ignoring offending hostkey by configuration");
            }
        }],

        # Found login string
        [$USERNAME_PROMPT, sub {

            if (($USER eq '') && ($USER_COUNT)) {
                ctrl("CLOSE:LOGIN:Empty username is not valid to login here");
                _hardClose($EXP, $UUID);
            }
            $USER_COUNT++;
            $USER = subst($USER);
            ctrl("LOGIN:$USER");
            send_slow($EXP, "$USER\n");
            exp_continue();
        }],

        # Found password/passphrase string
        [$PASSWORD_PROMPT, sub {
            # A few attempts...
            if ($PASSWORD_COUNT < 6) {
                my $password = '';
                my @before = split /\n/, $EXP->before;

                $PASSWORD_COUNT++;
                if ($proxy_jump) {
                    # get the next password
                    $password = getJumpPassword();
                } else {
                    if ((!$PASS) || ($PASS eq '<<ASK_PASS>>')) {
                        $password = askPassword();
                    } else {
                        $password = $PASS;
                    }
                }
                if ($password) {
                    $EXP->log_stdout(0);
                    $password = subst($password);
                    send_slow($EXP, "$password\n", 'hide');
                    $EXP->log_stdout(1);
                    ctrl("PASSWORD:Sent (not shown)");
                    if (!$proxy_jump) {
                        # Next time we will ask the password since the automatic one did not work
                        $PASS = '<<ASK_PASS>>';
                    }
                    exp_continue();
                } else {
                    ctrl("CLOSE:PASSWORD:Password input cancelled by user");
                    _hardClose($EXP, $UUID);
                }
            } else {
                # ... too many password attempts: provided password is no longer valid!!
                ctrl("CLOSE:PASSWORD:Provided username/password '$USER/<<hidden_password>>' was rejected");
                _hardClose($EXP, $UUID);
            }
        }],

        # Found user prompt
        [$COMMAND_PROMPT, sub {
            $CONNECTED = 1;

            if ($connection_cmd2) {
                # Pseudo Jump Server
                send_slow($EXP, "$connection_cmd2\n");
                $connection_cmd2 = '';
                $PSEUDO_JUMP = 1;
                $EXP->clear_accum();
                exp_continue();
            } elsif ($PSEUDO_JUMP && $EXP->before() =~ /\x1B$/) {
                # On pseudo jumps, the strings come with terminal escape sequences that match the user prompt regex
                $EXP->clear_accum();
                exp_continue();
            } else {
                my $avoid_first_expectation = 0;

                # Do we have to do complex expects?
                if (!$EXPECT || !$CONNECTED) {
                    return 1;
                }

                $EXP->restart_timeout_upon_receive(1);
                for(my $i = 0; $i < scalar(@{$$CFG{'environments'}{$UUID}{'expect'}}); $i++) {
                    my $hash = $$CFG{'environments'}{$UUID}{'expect'}[$i];
                    my $pattern = $$hash{'expect'} // '';
                    my $command = $$hash{'send'} // '';
                    my $hide = $$hash{'hidden'} // 0;
                    my $active = $$hash{'active'} // 0;
                    my $return = $$hash{'return'} // 1;
                    my $on_match = $$hash{'on_match'} // -1;
                    my $on_fail = $$hash{'on_fail'} // -1;
                    my $time_out = $$hash{'time_out'} // -1;

                    if (!$active) {
                        next;
                    }

                    my $skip_command = 0;

                    if ($time_out >= 0) {
                        $TIMEOUT_CMD = $time_out;
                    }
                    $pattern = subst($pattern);

                    ctrl("EXPECT:WAITING:$pattern");

                    # Wait for pattern prompt before continue...
                    $EXP->expect($TIMEOUT_CMD,

                        [timeout => sub {
                            if ($on_fail == -1) {
                                ctrl("CLOSE:TIMEOUT:$TIMEOUT_CMD seconds expecting pattern '$pattern'!!");
                                $CONNECTED = 0;
                                _hardClose($EXP, $UUID);
                            } elsif ($on_fail == -2) {
                                ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Finishing Expect");
                                $CONNECTED = 1;
                                $end = 1;
                            } else {
                                ctrl("EXPECT:ON_FAIL:timeout expecting '$pattern'. Jumping to '$on_fail'");
                                $i = --$on_fail;
                                $skip_command = 1;
                            }
                        }],

                        [eof => sub {
                            $CONNECTED = 0;
                            ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
                            _hardClose($EXP, $UUID);
                        }],

                        # Found Host-Key verification string
                        [$HOSTCHANGE_PROMPT, sub {

                            my $match = $EXP->match();
                            $match =~ /$HOSTCHANGE_PROMPT/g;
                            my ($yes, $no) = ($1, $2);
                            if ($ACCEPT_KEY) {
                                send_slow($EXP, "$yes\n");
                                ctrl("EXPECT:HOSTKEY:accepted by configuration (sent '$yes')");
                                exp_continue();
                            } else {
                                send_slow($EXP, "$no\n");
                                $CONNECTED = 0;
                                _hardClose($EXP, $UUID);
                                ctrl("CLOSE:EXPECT:HOSTKEY:rejected by configuration (sent '$no')");
                            }
                        }],

                        [($avoid_first_expectation) ? '' : $pattern, sub {
                            if ($on_match == -1) {
                                $skip_command = 0;
                            } elsif ($on_match == -2) {
                                $end = 1;
                                $CONNECTED = 1;
                                ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Stopping by config");
                            } else {
                                ctrl("EXPECT:ON_MATCH:found pattern '$pattern'. Jumping to '$on_match'");
                                $avoid_first_expectation = 1;
                                $i = --$on_match;
                            }
                        }]
                    );
                    if ($end) {
                        last;
                    }
                    if ($skip_command) {
                        next;
                    }
                    $avoid_first_expectation = 0;
                    if (!$CONNECTED) {
                        last;
                    }

                    $command = subst($command);

                    # Disconnect if an error occured during command substitution (eg: when user cancelled data entry)
                    if (not defined($command)) {
                        $CONNECTED = 0;
                        last;
                    }

                    # otherwise launch command
                    if ($hide) {
                        ctrl("EXPECT:SENDING:<<HIDDEN STRING>>" . ($return ? '\n' : ''));
                        $EXP->log_stdout(0);
                    } else {
                        my $cmd_str = $command;
                        $cmd_str =~ s/\n$//go;
                        ctrl("EXPECT:SENDING:$cmd_str" . ($return ? '\n' : ''));
                    }

                    send_slow($EXP, $command);
                    if ($return) {
                        send_slow($EXP, "\n");
                    }

                    if ($hide) {
                        $EXP->log_stdout(1);
                    }
                }

                $EXP->restart_timeout_upon_receive(0);
            }
        }],

    ); # EXPECT CLOSE

    if ($end || ! $error) {
        if ($CONNECTED) {
            ctrl("TITLE:$TITLE");
            ctrl("CONNECTED");
        }
    } else {
        $CONNECTED = 0;
        ctrl("DISCONNECTED:$error");
    }
} else {    # TODO this should be an elsif
    if (! defined $EXP->error) {
        $CONNECTED = 0;
        ctrl("DISCONNECTING:" . ($EXP->error));
    } else {
        $CONNECTED = 1;
        ctrl("CONNECTING:Manual authentication requested");
        ctrl("TITLE:$TITLE");
        ctrl("CONNECTED");
    }
}

$SIG{'WINCH'} = sub {
    if (!$CONNECTED) {
        return 1;
    }
    while (! $EXP->slave) {
        select(undef, undef, undef, 0.25);
    };
    $EXP->slave->clone_winsize_from(\*STDIN);
    kill WINCH => $EXP->pid if $EXP->pid;
};

$SIG{'INT'} = undef;
$SIG{'HUP'} = sub {
    # Avoid more interruptions
    local $SIG{'WINCH'} = undef;

    my $chain_uuid = '';
    my $CHAIN_CFG;

    _hardClose(undef, $UUID);

    if ($INT) {
        return 1;
    }
    $INT = 1;

    # First, read the file with the configuration to use
    my $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    sysread($SOCKET, $chain_uuid, 1024);
    $chain_uuid =~ s/^!!_PAC_CHAIN_\[(.+)\]!!$/$1/g;
    if (! $chain_uuid) {
        $INT = 0;
        return 1;
    }

    # Second, retrieve the 'serialized' configuration to be used
    $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    eval {$CHAIN_CFG = fd_retrieve($SOCKET);};
    if ($@) {
        $INT = 0;
        return 1;
    }

    # Prepare some progressbar data
    my $chain_name = $$CHAIN_CFG{'environments'}{$chain_uuid}{'name'};
    my $exp_partial = 0;
    my $exp_total = 0;
    foreach my $exp (@{$$CHAIN_CFG{'environments'}{$chain_uuid}{'expect'}}) {
        if ($$exp{'active'} // 0) {
            ++$exp_total;
        }
    }
    if (! $exp_total) {
        $INT = 0;
        return 1;
    }
    ctrl("CHAIN:$chain_name:$chain_uuid:$exp_partial:$exp_total");

    my $TIMEOUT_CMD = $$CHAIN_CFG{'defaults'}{'timeout command'} || undef;
    if ($$CHAIN_CFG{'environments'}{$chain_uuid}{'terminal options'}{'use personal settings'}) {
        $TIMEOUT_CMD = $$CHAIN_CFG{'environments'}{$chain_uuid}{'terminal options'}{'timeout command'} || undef;
    }

    my $end = 0;
    my $avoid_first_expectation = 1;
    $EXP->restart_timeout_upon_receive(1);
    for(my $i = 0; $i < scalar(@{$$CHAIN_CFG{'environments'}{$chain_uuid}{'expect'}}); $i++) {
        my $hash = $$CHAIN_CFG{'environments'}{$chain_uuid}{'expect'}[$i];
        my $pattern = $$hash{'expect'} // '';
        my $command = $$hash{'send'} // '';
        my $hide = $$hash{'hidden'} // 0;
        my $active = $$hash{'active'} // 0;
        my $return = $$hash{'return'} // 1;
        my $on_match = $$hash{'on_match'} // -1;
        my $on_fail = $$hash{'on_fail'} // -1;
        my $time_out = $$hash{'time_out'} // -1;

        if (!$active) {
            next;
        }
        my $skip_command = 0;

        if ($time_out >= 0) {
            $TIMEOUT_CMD = $time_out;
        }

        $pattern = subst($pattern);

        ctrl("CHAIN:$chain_name:WAITING($pattern):" . ($exp_partial++) . ":$exp_total");

        # Wait for pattern prompt before continue...
        $EXP->expect($TIMEOUT_CMD,

            [timeout => sub {
                if ($on_fail == -1) {
                    ctrl("CLOSE:TIMEOUT:$TIMEOUT_CMD seconds expecting pattern '$pattern'!!");
                    $CONNECTED = 0;
                    _hardClose($EXP, $UUID);
                } elsif ($on_fail == -2) {
                    ctrl("CHAIN:EXPECT:ON_FAIL:timeout expecting '$pattern'. Finishing Expect");
                    $CONNECTED = 1;
                    $end = 1;
                } else {
                    ctrl("CHAIN:EXPECT:ON_FAIL:timeout expecting '$pattern'. Jumping to '$on_fail'");
                    $i = --$on_fail;
                    $skip_command = 1;
                }
            }],

            [eof => sub {

                $CONNECTED = 0;
                ctrl("CLOSE:Connection ended by remote peer!! " . $EXP->set_accum());
                _hardClose($EXP, $UUID);
            }],

            # Found Host-Key verification string
            [$HOSTCHANGE_PROMPT, sub {

                my $match = $EXP->match();
                $match =~ /$HOSTCHANGE_PROMPT/go;
                my ($yes, $no) = ($1, $2);
                if ($ACCEPT_KEY) {
                    send_slow($EXP, $yes . (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) ? "\r\f" : "\n");
                    ctrl("EXPECT:HOSTKEY:accepted by configuration (sent '$yes')");
                    exp_continue();
                } else {
                    send_slow($EXP, $no . (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250')) ? "\r\f" : "\n");
                    $CONNECTED = 0;
                    _hardClose($EXP, $UUID);
                    ctrl("CLOSE:EXPECT:HOSTKEY:rejected by configuration (sent '$no')");
                }
            }],

            [($avoid_first_expectation) ? '' : $pattern, sub {
                if ($on_match == -1) {
                    $skip_command = 0;
                } elsif ($on_match == -2) {
                    $end = 1;
                    $CONNECTED = 1;
                    ctrl("CHAIN:EXPECT:ON_MATCH:found pattern '$pattern'. Stopping by config");
                } else {
                    ctrl("CHAIN:EXPECT:ON_MATCH:found pattern '$pattern'. Jumping to '$on_match'");
                    $avoid_first_expectation = 1;
                    $i = --$on_match;
                }
            }]
        ); # EXPECT CLOSE
        if ($end) {
            last;
        }
        if ($skip_command) {
            next;
        }
        $avoid_first_expectation = 0;
        if (!$CONNECTED) {
            last;
        }

        $command = subst($command);

        # ... and launch command
        if ($hide) {
            ctrl("CHAIN:$chain_name:SENDING(<<HIDDEN STRING>>):$exp_partial:$exp_total");
            $EXP->log_stdout(0);
        } else {
            my $cmd_str = $command;
            $cmd_str =~ s/\n$//go;
            ctrl("CHAIN:$chain_name:SENDING:$cmd_str" . ($return ? '\n' : '') . ":$exp_partial:$exp_total");
        }

        send_slow($EXP, $command . ($return ? (($METHOD =~ /^.*3270.*$/) || ($METHOD eq 'IBM 3270/5250') ? "\r\f" : "\n") : ''));
    }

    $EXP->log_stdout(0);
    $EXP->restart_timeout_upon_receive(0);

    if ($$CHAIN_CFG{'tmp'}{'set title'}) {
        ctrl("TITLE:$$CHAIN_CFG{'tmp'}{'title'}");
    }
    ctrl("CONNECTED");
    $INT = 0;
    return 1;
};

$SIG{'USR1'} = sub {
    # Avoid more interruptions
    local $SIG{'WINCH'} = undef;

    return 1 if $INT;
    $INT = 1;

    # Now, read the command to execute
    my $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    my $tmp;
    eval {
        $tmp = fd_retrieve($SOCKET);
    };
    if ($@) {
        return 1;
    }
    if (! defined $$tmp{cmd}) {
        $INT = 0;
        return 1;
    }

    _execAndCapture($tmp);

    ctrl($CONNECTED ? "CONNECTED" : "DISCONNECTED");
    $INT = 0;
    return 1;
};

$SIG{'USR2'} = sub {
    # Avoid more interruptions
    local $SIG{'WINCH'} = undef;

    return 1 if $INT;
    $INT = 1;

    my $rin = '';
    vec($rin, fileno($SOCKET), 1) = 1;
    select($rin, undef, undef, 2) or return 1;
    my $tmp;
    $tmp = fd_retrieve($SOCKET) or return 0;
    if (! defined $tmp) {
        $INT = 0;
        return 0;
    }

    _execScript($tmp);
    ctrl($CONNECTED ? "CONNECTED" : "DISCONNECTED");

    $INT = 0;
    return 1;
};

if ($CONNECTED) {
    # Restart only when connecting at startup connection
    if ($HIDE_TERMINAL) {
        ctrl("HIDE_TERMINAL");
    }
    if ($IS_CLUSTER && ($RESTART == 2)) {
        $RESTART = 0;
    }
    $EXP->interact(\*STDIN, "__PAC__STOP__${UUID}__${$}__");
}

_hardClose(undef, $UUID);

my $why = $?;

if (($why ne 0) && $RESTART) {
    ctrl('RESTART');
}

# Finish this expect session
if (defined $EXP->pid) {
    kill(15, $EXP->pid);
}
$EXP->hard_close();

close_log_file();

ctrl("DISCONNECTED");

exit 0;

sub close_log_file {
    my $line;

    if ($LOG_TIMESTAMP) {
        close LOG;
    }
    if (!$LOG_FILE || !$REMOVE_CTRL_CHARS) {
        return 1;
    }
    if (!open(FIN,"<:raw",$LOG_FILE)) {
        ctrl("ERROR: Could not open file '$LOG_FILE' for reading!! ($!)");
        return 1;
    }
    if (!open(FOUT,">:raw","$LOG_FILE.$$")) {
        ctrl("ERROR: Could not open file '$LOG_FILE.$$' for writting!! ($!)");
        close FIN;
        return 1;
    }
    while ($line = <FIN>) {
        my $l = _removeEscapeSeqs($line);
        if ($LOG_TIMESTAMP) {
            while ($l =~ s/^(\D.+?)\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} : /$1/) {}
            if ($l !~ /^\d{3,4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} : /) {
                print FOUT ' 'x22;
            }
        }
        print FOUT $l;
    }
    close FIN;
    close FOUT;
    rename("$LOG_FILE.$$", $LOG_FILE) or ctrl("ERROR: $!");
}

# END : Main program
########################################################

__END__

=encoding utf8

=head1 NAME

asbru_conn

=head1 SYNOPSIS

Application to open a connection of type : ssh, vnc, sftp, telnet, ftp, rdp, etc.

=head1 DESCRIPTION

Global Variables

    %COLOR          Colors to use in terminal messages
                    'norm'    black
                    'log'     yellow
                    'recv'    magenta
                    'sent'    green
                    'err'     red
    $EXP            Expect object
    $GSETTINGS      Gnome settings
    $CFG_FILE       Configuration file.freeze passed as shell argument
    $UUID           session UUID as shell argument
    $GETCMD         command to execute  as shell argument
    $DEBUG          0 No , 1 Yes : Set with option "Expect : DEBUG" in Terminal options Advanced
    $CFG            Configuration object "$CFG = retrieve($CFG_FILE)"
    $SEND_SLOW      Time in milliseconds to wait between


=head2 sub __disconnect

Handle disconnection

=head2 sub msg

prints a message to the terminal screen using I<log color>

=head2 sub ctrl

Sends a internal message to the terminal associated with this connection. Using a UNIX socket

=head2 sub receiveData

Read data comming from the socket, used to prompt data to the user

=head2 sub auth (socket)

Check that socket is a PAC socket and not other
    return true 1, false 0

=head2 sub subst (line)

B<line> string

    takes "line" and substitutes tags for their values
        password
        username
        uuid
        global and local variables
        kepass masks
        etc

=head2 sub wEnterValue(undef,title,label,default,visible)

Display a modal input dialog box and wait for answer

B<title> Title to your request

B<label> The question to answer

B<default> if defined, will search for an array with that name and create a Listbox to choose from predefined values, otherwise will draw a textbox for input

B<visible> if the text typed by the user should be visible or hidden (passwords should be hidden)

=head2 sub send_slow(expect object,message)

B<expect object> has attached a spawned connection
B<message> message to send to the spawned connection

    if ($SEND_SLOW) {
        expect->send_slow(message)
    } else {
        expect->send(message)
    }

=head2 sub _getPrompt

Sends a "\n" to the current session and waits for the answer

runs expect expressions over the answer

=head2 sub _execAndCapture

Execute a command in the session and wait for answer

Pipe the command to other terminals if $pipe is active (possibly used in clusters)

=head2 sub _execScript

Parse a user script, run the script.

=head2 sub _setProxy

Preparse proxy connections depending on the type of proxy used, currently : SOCKS and JumpHost (-J)

Is uses a local config file to preconfigure the parameters.

=head2 main

Steps

    Create a connection string depending on:
        Proxy
        Protocol (telnet, ssh, rdp, etc)
        Terminal session options
        Credentials
        tunnels
        ...
    Spawn the session using the Expect object : $EXP
        Inform the terminal that the spawned object has been created
    Depending on the protocol follow user authentication
        Supply user validation data as expect finds the login patterns
    Create signal handlers
    If connected
        Send interaction to Terminal


=head1 Perl particulars

    select(undef, undef, undef, 0.5)        ===>  sleep(500ms);
